package com.boredom.Guts;

import java.io.*;
import java.security.*;
import javax.crypto.*;
import com.boredom.Guts.InternalException;

class InternalException extends RuntimeException
{
    InternalException(String message, Throwable cause) {
	super(message, cause);
    }

    InternalException(Throwable cause) {
	super(cause);
    }
}

// Notice how checked exceptions are wrapped in unchecked InternalExeptions,
// rather than just propogated up. There are two reasons for this:
//   1. If one of these exceptions occurred, something went terribly wrong, and
//      we probably can't deal with it anyways.
//   2. If we _do_ decide to do something about these errors, like tell the
//      user, we probably want to treat them uniformly.

/**
 * Provide basic signature and encryption functionality
 *
 * An Identity represents either an individual user, or a user group.
 * It can sign objects so that others know they originated from it,
 * and it can encrypt objects so that only identical Identities can access it.
 */
public class Identity implements Serializable
{
    private static String SYMMETRIC_ALGORITHM = "AES";
    private static String ASYMMETRIC_ALGORITHM = "DiffieHellman";

    // Your public, private, and secret keys.  They should be stored persistently.
    private PublicKey publicKey;
    private PrivateKey privateKey;
    private SecretKey secretKey;

    // Used both for signing Objects and verifying others' signatures.
    private Signature signer;

    // Used both from encrypting and decrypting objects with your shared secret key.
    private Cipher cipher;

    /**
     * The installation constructor. Generates fresh keys. Call this precisely *once*,
     * during installation.
     */
    public Identity() {
	generateKeys();
	init();
    }

    private void init() {
	try {
	    signer = Signature.getInstance(ASYMMETRIC_ALGORITHM);
	    cipher = Cipher.getInstance(SYMMETRIC_ALGORITHM);
	}
	catch (NoSuchAlgorithmException e) {
	    throw new InternalException(e);
	}
	catch (NoSuchPaddingException e) {
	    throw new InternalException(e);
	}
    }

    private void generateKeys() {
	try {
	    KeyPair keyPair = KeyPairGenerator.getInstance(ASYMMETRIC_ALGORITHM).generateKeyPair();
	    SecretKey key = KeyGenerator.getInstance(SYMMETRIC_ALGORITHM).generateKey();
	    publicKey = keyPair.getPublic();
	    privateKey = keyPair.getPrivate();
	    secretKey = key;
	}
	catch (NoSuchAlgorithmException e) {
	    throw new InternalException(e);
	}
    }

    /**
     * Get the Identity's public key. The key can be used to verify its signatures.
     */
    public PublicKey getPublicKey() {
	return publicKey;
    }

    /**
     * Sign an object so that others can verify that it came from you.
     */
    public byte[] sign(Object object) {
	return sign(serialize(object));
    }

    private byte[] sign(byte[] data) {
	try {
	    signer.initSign(privateKey);
	    signer.update(data);
	    return signer.sign();
	}
	catch (SignatureException e) {
	    throw new InternalException(e);
	}
	catch (InvalidKeyException e) {
	    throw new InternalException(e);
	}
    }

    /**
     * Verify that an object was signed by the Identity you thought it was.
     */
    public boolean verify(Object object, byte[] signature, PublicKey key) {
	return verify(serialize(object), signature, key);
    }

    private boolean verify(byte[] data, byte[] signature, PublicKey key) {
	try {
	    signer.initVerify(key);
	    signer.update(data);
	    return signer.verify(signature);
	}
	catch (Exception exception) {
	    return false;
	}
    }

    /**
     * Encrypt an object so that only others with the same Identity can access it.
     */
    public SealedObject encrypt(Serializable object) {
	try {
	    cipher.init(Cipher.ENCRYPT_MODE, secretKey);
	    return new SealedObject(object, cipher);
	}
	catch (InvalidKeyException e) {
	    throw new InternalException(e);
	}
	catch (IOException e) {
	    throw new InternalException(e);
	}
	catch (IllegalBlockSizeException e) {
	    throw new InternalException(e);
	}
    }

    /**
     * Decrypt an object using your secret key.
     * Return null on failure.
     */
    public Object decrypt(SealedObject sealed) {
	try {
	    return sealed.getObject(secretKey);
	}
	catch (IOException e) {
	    return null;
	}
	catch (NoSuchAlgorithmException e) {
	    return null;
	}
	catch (InvalidKeyException e) {
	    return null;
	}
	catch (ClassNotFoundException e) {
	    throw new InternalException(e);
	}
    }

    /**
     * Convert an Object into an array of bytes.
     * From SUN's web site, referenced at http://www.javafaq.nu/java-article236.html
     */
    private static byte[] serialize(Object object) {
	try {
	    ByteArrayOutputStream byteStream = new ByteArrayOutputStream();
	    ObjectOutputStream stream = new ObjectOutputStream(byteStream);
	    stream.writeObject(object);
	    stream.flush();
	    stream.close();
	    byteStream.close();
	    byte[] data = byteStream.toByteArray();
	    return data;
	}
	catch (IOException e) {
	    throw new InternalException(e);
	}
    }
}
